﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Data;

namespace Sworm
{
    public class MetadataBuilder
    {
        private class SingletonHolder<TDialect, TAnalyzer>
            where TDialect : SqlDialect, new()
            where TAnalyzer : TypeAnalyzer, new()
        {
            public static MetadataBuilder Instance = new MetadataBuilder(new TDialect(), new TAnalyzer());
        }

        public static readonly MetadataBuilder Default = SingletonFor<SqlDialect, TypeAnalyzer>();

        /// <summary>
        /// Returns a singleton <see cref="MetadataBuilder"/> for the given
        /// types of <see cref="SqlDialect"/> and <see cref="TypeAnalyzer"/>.
        /// </summary>
        /// <typeparam name="TDialect"></typeparam>
        /// <typeparam name="TAnalyzer"></typeparam>
        /// <returns></returns>
        public static MetadataBuilder SingletonFor<TDialect, TAnalyzer>()
            where TDialect : SqlDialect, new()
            where TAnalyzer : TypeAnalyzer, new()
        {
            return SingletonHolder<TDialect, TAnalyzer>.Instance;
        }

        /// <summary>
        /// There should only be one instance of this class for a given pair of
        /// <see cref="SqlDialect"/> and <see cref="TypeAnalyzer"/>.
        /// Rebuilding metadata all the time would get expensive.
        /// Prefer the <see cref="SingletonFor{TDialect, TAnalyzer}()"/> method if possible.
        /// </summary>
        /// <param name="dialect"></param>
        /// <param name="analyzer"></param>
        public MetadataBuilder(SqlDialect dialect, TypeAnalyzer analyzer)
        {
            this.Dialect = dialect;
            this.analyzer = analyzer;
        }

        public SqlDialect Dialect { get; private set; }
        private readonly TypeAnalyzer analyzer;

        private Dictionary<Type, TableData> cache = new Dictionary<Type, TableData>();

        public bool ShouldMapTable(Type tableType)
        {
            return analyzer.ShouldMapTable(tableType);
        }

        private TableData GetWithoutRelationships(Type tableType)
        {
            if (!ShouldMapTable(tableType))
            {
                throw new ArgumentException(string.Format(
                    "{0} is not a mappable class", tableType.FullName));
            }

            if (cache.ContainsKey(tableType))
            {
                return cache[tableType];
            }

            var table = new TableData();
            table.ClassType = tableType;
            string rawName = GetTableName(tableType);
            table.Name = new SqlIdentifier(rawName, Dialect.EscapeTableName(rawName));
            SetupColumns(table, tableType);

            bool autoIncrement;
            table.OptionalData.PrimaryKey = SelectPrimaryKey(table, out autoIncrement);
            if (table.OptionalData.PrimaryKey != null)
            {
                table.OptionalData.HasAutoIncrementPK = autoIncrement;
            }

            cache[tableType] = table;
            return table;
        }

        public TableData TableDataFor(Type tableType)
        {
            var table = GetWithoutRelationships(tableType);
            SetupRelationships(table, tableType);
            return table;
        }

        private string GetTableName(Type type)
        {
            return analyzer.GetTableName(type);
        }

        private void SetupColumns(TableData table, Type tableType)
        {
            var members = tableType.SelectWritableMembers().ToList();

            table.Columns = new List<ColumnData>();
            foreach (var member in members)
            {
                if (!ShouldMapColumn(member))
                {
                    continue;
                }
                var sqlType = GetSqlType(member);
                if (sqlType == null)
                {
                    continue;
                }

                var column = new ColumnData();
                column.Member = member;

                string rawName = GetSqlColumnName(member);
                column.Name = new SqlIdentifier(rawName, Dialect.EscapeColumnName(rawName));

                column.SqlType = sqlType.Value;
                column.Table = table;
                table.Columns.Add(column);

                SetupConverters(column);
            }
        }

        private void SetupConverters(ColumnData column)
        {
            var toDB = GetDefaultToDbConverter(column.Member.Type);
            if (toDB != null)
            {
                column.OptionalData.ToDbConverter = toDB;
            }

            var fromDB = GetDefaultFromDbConverter(column.Member);
            if (fromDB != null)
            {
                column.OptionalData.FromDbConverter = fromDB;
            }
        }

        private bool ShouldMapColumn(Member member)
        {
            return analyzer.ShouldMapColumn(member);
        }

        private string GetSqlColumnName(Member member)
        {
            return analyzer.GetSqlColumnName(member);
        }

        private DbType? GetSqlType(Member member)
        {
            return analyzer.GetDbType(member) ?? GetDefaultSqlType(member.Type);
        }

        private DbType? GetDefaultSqlType(Type clrType)
        {
            analyzer.SimplifyColumnType(ref clrType);

            if (Dialect.IsColumnType(clrType))
            {
                return Dialect.GetDbTypeFor(clrType);
            }
            return null;
        }

        private void SetupRelationships(TableData table, Type tableType)
        {
            if (table.Relationships != null)
            {
                return;
            }
            table.Relationships = new List<RelationshipData>();

            var members = tableType.SelectWritableMembers().Where(ShouldMapRelationship).ToList();

            foreach (var member in members)
            {
                var relationship = new RelationshipData();
                relationship.SourceMember = member;
                table.Relationships.Add(relationship);

                var cols = GetJoinColumn(table, member);
                relationship.SourceColumn = cols.Item1;
                relationship.DestinationColumn = cols.Item2;

                TrySetDestinationMember(relationship);
            }

            foreach (var relationship in table.Relationships)
            {
                SetupRelationships(relationship.DestinationTable, relationship.DestinationType);
            }
        }

        private bool ShouldMapRelationship(Member member)
        {
            return ShouldMapTable(member.Type) && analyzer.ShouldMapRelationship(member);
        }

        private Tuple<ColumnData, ColumnData> GetJoinColumn(TableData sourceTable, Member member)
        {
            var destType = member.Type;
            if (member.IsCollection)
            {
                destType = member.CollectionOfType;
            }

            var destTable = GetWithoutRelationships(destType);

            ColumnData sourceColumn = null;
            if (!member.IsCollection)
            {
                sourceColumn = TryFindColumnBackingForHasOneRelationship(sourceTable, member);
            }

            // If this member is a collection, we know we have to find the fk on the dest side.
            // If it's not a collection and we didn't find it on the source side, we have to
            // find the other side of a 1-1 on the dest side.
            if (member.IsCollection || sourceColumn == null)
            {
                sourceColumn = SelectPrimaryKey(sourceTable);

                var candidates = destTable.ClassType.SelectWritableMembers()
                    .Where(m => m.Type == sourceTable.ClassType) // function IsNontriviallyAssignable, use RelevantAssemblies
                    .ToList();

                if (candidates.Count != 1)
                {
                    throw new NotSupportedException("can't do this without additinal info, let's see how common it is");
                }

                var destColumn = TryFindColumnBackingForHasOneRelationship(destTable, candidates.Single());
                if (destColumn == null)
                {
                    throw new Exception("TODO this relationship is complicated (or incorrect)");
                }

                return new Tuple<ColumnData, ColumnData>(sourceColumn, destColumn);
            }
            else
            {
                var destColumn = SelectPrimaryKey(destTable);
                return new Tuple<ColumnData, ColumnData>(sourceColumn, destColumn);
            }
        }

        private ColumnData SelectPrimaryKey(TableData table)
        {
            bool dummy;
            return SelectPrimaryKey(table, out dummy);
        }

        private ColumnData SelectPrimaryKey(TableData table, out bool isAutoIncrement)
        {
            return analyzer.SelectPrimaryKey(table, out isAutoIncrement);
        }

        private static void TrySetDestinationMember(RelationshipData relationship)
        {
            var candidates = relationship.DestinationType.SelectWritableMembers()
                .Where(m => m.CollectionOfType == relationship.SourceType)
                .ToList();

            if (candidates.Count == 1)
            {
                relationship.DestinationMember = candidates.Single();
            }
        }

        private ColumnData TryFindColumnBackingForHasOneRelationship(TableData table, Member hasOneRelationship)
        {
            string cand1 = hasOneRelationship.Name + "_id";
            string cand2 = hasOneRelationship.Name + "id";

            return table.Columns.SingleOrDefault(c => 
                c.Name.Unescaped.EqualsIgnoreCase(cand1) || c.Name.Unescaped.EqualsIgnoreCase(cand2));
        }

        public Func<object, object> GetDefaultToDbConverter(Type paramType)
        {
            return analyzer.GetDefaultToDbConverter(paramType);
        }

        private Func<DbAccessor, object> GetDefaultFromDbConverter(Member member)
        {
            return analyzer.GetFromDbConverter(member);
        }
    }
}
